/**
 * @description 热更新管理器
 * @date 2024.9.10
 * @author wz
 * @midify 2025.3.15
 * @editor wz
 */
import { game, JsonAsset, native, resources, sys } from 'cc';
import { Singleton } from '../../utils/Singleton';
import { HotConst, HotState } from './BaseTypes';

export class HotMgr extends Singleton {
    /**
     * 本地热更文件夹路径
     */
    private _storagePath: string = '';
    /**
     * 本地热更配置文件路径
     */
    private _storageManifestPath: string = '';
    /**
     * 热更管理器
     */
    private _am: native.AssetsManager = null;
    /**
     * 版本号
     */
    private _version: string = '';
    /**
     * 下载失败文件的数量
     */
    private _failCount: number = 0;

    /**
     * 热更检查回调
     */
    private _checkListener: (state: HotState, totalBytes?: number) => void = null;
    /**
     * 热更新回调
     */
    private _updateListener: (downBytes: number, totalBytes: number) => void = null;


    constructor() {
        super();
        if (this._checkEnv()) {
            this._storagePath = native.fileUtils.getWritablePath() + '/' + HotConst.localHotDir;
            this._storageManifestPath = this._storagePath + '/' + HotConst.projectManifest;
        }
    }



    /**
     * 初始化
     * @param url 热更新地址
     * @param verion 热更新版本号
     * @returns 
     */
    private async _init(url: string, projectManifest: any) {
        // 设置本地最新远程热更地址
        projectManifest.packageUrl = url;
        projectManifest.remoteManifestUrl = url + '/' + HotConst.projectManifest + '?t=' + Date.now();
        projectManifest.remoteVersionUrl = url + '/' + HotConst.versionManifest + '?t=' + Date.now();

        // 生成本地热更文件夹路径
        if (!native.fileUtils.isDirectoryExist(this._storagePath))
            native.fileUtils.createDirectory(this._storagePath)

        // 保存至本地文件
        native.fileUtils.writeStringToFile(JSON.stringify(projectManifest), this._storageManifestPath)

        // 创建am,设置本地manifest路径以及本地热更文件夹路径
        this._am = new native.AssetsManager(this._storageManifestPath, this._storagePath, this._versionCb.bind(this))

        // this._am.loadLocalManifest(localManifestPath); ==> 在创建am时指定了本地manifest路径不用再设置了
        // this._am.setVerifyCallback(this._verifyCb)
    }

    /**
     * 环境检查
     */
    private _checkEnv() {
        if (!sys.isNative) {
            console.log('环境不支持热更新');
            return false;
        }
        return true;
    }

    /**
     * 获取本地projectManifest
     */
    private _getProjectManifest(callback: (manifest: any) => void) {
        let projectManifest: any = {};
        // 有缓存的文件
        if (native.fileUtils.isFileExist(this._storageManifestPath)) {
            projectManifest = JSON.parse(native.fileUtils.getStringFromFile(this._storageManifestPath))
            callback && callback(projectManifest)
        }
        else {
            resources.load(HotConst.resourcesManifestPath, JsonAsset, (err, data: JsonAsset) => {
                if (err) {
                    projectManifest.assets = {};
                    projectManifest.searchPaths = [];
                    projectManifest.version = HotConst.package_version;
                    callback && callback(projectManifest)
                } else {
                    callback && callback(data.json)
                }
            })
        }

    }


    /**
     * 默认版本比较
     * @param versionA 
     * @param versionB 
     * @returns 
     */
    private _versionCb(versionA: string, versionB: string) {
        console.log("JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB);
        let vA = versionA.split('.');
        let vB = versionB.split('.');
        for (let i = 0; i < vA.length; ++i) {
            let a = parseInt(vA[i]);
            let b = parseInt(vB[i] || '0');
            if (a === b) {
                continue;
            }
            else {
                return a - b;
            }
        }
        if (vB.length > vA.length) {
            return -1;
        }
        else {
            return 0;
        }
    }

    /**
     * 验证回调
     * @param path 
     * @param asset 
     */
    private _verifyCb(path: string, asset: any) {
        // When asset is compressed, we don't need to check its md5, because zip file have been deleted.
        var compressed = asset.compressed;
        // Retrieve the correct md5 value.
        var expectedMD5 = asset.md5;
        // asset.path is relative path and path is absolute.
        var relativePath = asset.path;
        // The size of asset file, but this value could be absent.
        var size = asset.size;
        if (compressed) {
            console.log("Verification passed : " + relativePath);
            return true;
        }
        else {
            console.log("Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
            return true;
        }
    }


    private _checkCb(event: native.EventAssetsManager) {
        const eventCode = event.getEventCode();
        console.log('checkCb Code: ' + eventCode);
        switch (eventCode) {
            case native.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                console.warn("No local manifest file found, hot update skipped.");
                break;
            case native.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                console.warn("Fail to download manifest file, hot update skipped0.");
                break
            case native.EventAssetsManager.ERROR_PARSE_MANIFEST:
                console.warn("Fail to download manifest file, hot update skipped1.");
                break;
            case native.EventAssetsManager.ALREADY_UP_TO_DATE:
                console.warn("Already up to date with the latest remote version.");
                break;
            case native.EventAssetsManager.NEW_VERSION_FOUND:
                console.warn('New version found, please try to update. (' + Math.ceil(this._am.getTotalBytes() / 1024) + 'kb)');
                break;
            default:
                return;
        }

        this._am.setEventCallback(null);

        let state = HotState.NONE;
        let totalBytes = 0;

        // 有新版本
        if (eventCode == native.EventAssetsManager.NEW_VERSION_FOUND && this._am.getTotalBytes() > 0) {
            state = HotState.HOT;
            totalBytes = this._am.getTotalBytes();
        }

        // 回调
        if (this._checkListener) {
            this._checkListener(state, totalBytes);
            this._checkListener = null;
        }
    }

    private _updateCb(event: native.EventAssetsManager) {
        let needRestart = false;
        console.log('updateCb Code: ' + event.getEventCode());
        switch (event.getEventCode()) {
            case native.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                console.log('No local manifest file found, hot update skipped.');
                break;
            case native.EventAssetsManager.UPDATE_PROGRESSION:
                let byteProgress = event.getPercent();
                if (isNaN(byteProgress)) byteProgress = 0;

                let fileProgress = event.getPercentByFile();
                if (isNaN(fileProgress)) byteProgress = 0;

                let downFiles = event.getDownloadedFiles();
                if (isNaN(downFiles)) downFiles = 0;

                let totalFiles = event.getTotalFiles();
                if (isNaN(totalFiles)) totalFiles = 0;

                let downBytes = event.getDownloadedBytes();
                if (isNaN(downBytes)) downBytes = 0;

                let totalBytes = event.getTotalBytes();
                if (isNaN(totalBytes)) totalBytes = 0;

                if (this._updateListener && totalBytes > 0) this._updateListener(downBytes, totalBytes);
                console.log(`byteProgress:${byteProgress},fileProgress:${fileProgress},downFiles:${downFiles},totalFiles:${totalFiles},downBytes:${downBytes},totalBytes:${totalBytes}`)
                break;
            case native.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case native.EventAssetsManager.ERROR_PARSE_MANIFEST:
                console.log('Fail to download manifest file, hot update skipped.');
                break;
            case native.EventAssetsManager.ALREADY_UP_TO_DATE:
                console.log('Already up to date with the latest remote version.');
                break;
            case native.EventAssetsManager.UPDATE_FINISHED:
                console.log('Update finished. ' + event.getMessage());
                needRestart = true;
                break;
            case native.EventAssetsManager.UPDATE_FAILED:
                console.log('Update failed. ' + event.getMessage());
                this._failCount++;
                break;
            case native.EventAssetsManager.ERROR_UPDATING:
                console.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
                break;
            case native.EventAssetsManager.ERROR_DECOMPRESS:
                console.log('ERROR_DECOMPRESS:' + event.getMessage());
                break;
            default:
                break;
        }

        if (this._failCount > 10) {
            this._failCount = 0;
            this._am.downloadFailedAssets();
        }

        if (needRestart) {
            this._am.setEventCallback(null);

            // Prepend the manifest's search path
            var searchPaths = native.fileUtils.getSearchPaths();
            var newPaths = this._am.getLocalManifest().getSearchPaths();
            console.log(JSON.stringify(newPaths));
            Array.prototype.unshift.apply(searchPaths, newPaths);
            // This value will be retrieved and appended to the default search path during game startup,
            // please refer to samples/js-tests/main.js for detailed usage.
            // !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
            localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
            native.fileUtils.setSearchPaths(searchPaths);

            // restart game.
            setTimeout(() => {
                game.restart();
            }, 1000)
        }
    }

    /**
     * 在外面留一个清理热更缓存的安全按钮
     * @returns 
     */
    clearHotCacheAndRestart() {
        if (!this._checkEnv()) return;
        native.fileUtils.removeDirectory(this._storagePath);
        native.fileUtils.setSearchPaths([]);
        sys.localStorage.removeItem('HotUpdateSearchPaths');
        setTimeout(() => {
            game.restart();
        }, 1000)
    }

    /**
     * 更新检查
     * @description
     * 版本号规则 1.0.0 = build(1) + res(0.0)
     * 第一位==> 主版本号 ==> 包更新 ==> build
     * 第二位==> 新增功能 ==> 热更新 ==> res的第一位
     * 第三位==> bug修复  ==> 热更新 ==> res的第二位
     * @param url 热更新地址(http://sup.qqwlsq.com/1.1.1/1001/search_0/)
     * @param version 版本号
     * @param nativeVersion 版本号(包体本身版本号)
     * @param callback 
     * @returns 
     */
    checkUpdate(url: string, version: string, nativeVersion: string, callback: (state: HotState, totalBytes?: number) => void) {
        if (!this._checkEnv()) return;

        // 先获取本地manifest文件，获取本地的信息
        this._getProjectManifest((projectManifest: any) => {
            // 设置本地版本
            this._version = projectManifest.version

            // 当热更版本号低于apk版本的时候，清理一下热更缓存并重启一下，一般大更新覆盖安装的时候出现
            if (this._versionCb(version, nativeVersion) <= 0 && native.fileUtils.isDirectoryExist(this._storagePath)) {
                this.clearHotCacheAndRestart();
                return;
            }

            // 判断是否整包更新
            const versionAry = this._version.split('.');
            const remoteVersionAry = version.split('.');
            if (remoteVersionAry[0] > versionAry[0]) {
                callback && callback(HotState.FORCE);
                return;
            }

            this._init(url, projectManifest);
            if (this._am.getState() == native.AssetsManager.State.UNINITED) {
                console.log('Failed not init ...');
                return;
            }
            if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
                console.log('Failed to load local manifest ...');
                return;
            }
            this._checkListener = callback;
            this._am.setEventCallback(this._checkCb.bind(this));
            this._am.checkUpdate();

        })
    }


    /**
     * 开始热更新
     * @param callback 
     * @returns 
     */
    startHotUpdate(callback: (downBytes: number, totalBytes: number) => void) {
        if (!this._checkEnv()) return;
        if (this._am.getState() == native.AssetsManager.State.UNINITED) {
            console.log('Failed not init ...');
            return;
        }
        if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
            console.log('Failed to load local manifest ...');
            return;
        }

        this._failCount = 0;
        this._updateListener = callback;
        this._am.setEventCallback(this._updateCb.bind(this));
        this._am.update();
    }

    /**
     * 获取本地版本号
     * @returns 
     */
    getLocalVersion() {
        return this._version;
    }
}
/**
 * 热更新管理
 */
export const hotMgr: HotMgr = HotMgr.getInstance<HotMgr>()
